iT邦幫忙

2022 iThome 鐵人賽

DAY 3
0

昨天講到了定義域,今天換來討論值域。理想上一個方法都希望有一個回傳值,但現實中很難做到,就算已經在先驗條件中把所有未定義的輸入都排除了,仍然會有狀況是導致沒辦法得到對應的輸出(寫久了發現這通常牽扯到複雜的外部行為,以函數式風格來說就是不純),通常我們會使用null去解決這個問題。舉例來說,我要從資料庫中取出某個會員的資料,程式碼大概會這樣寫:

var person = _dbContext.Member.FirstOrDefault(x => x.Id == id);
Console.WriteLine(person.Name);

當取不到資料的時候,person就會是null reference的,概念上指向”空”,而後面如果沒有特別的處理,程式就會發生意料之外的崩潰。如果有維運過龐大的系統,應該多多少少有經驗處理過因為出乎意料的null reference造成的系統錯誤,花了大把精力排查問題才找到問題源頭。

那麼有沒有可能不使用null呢?我們再回來看看如何用函數表示回傳沒有回傳結果,感覺上好像很矛盾,但其實可以換一個念頭,所謂的沒有回傳結果就是一個結果,用圖的話可以這樣表示:

https://ithelp.ithome.com.tw/upload/images/20220914/20148594XttYzjS0vI.png

不要忘了,函數也可以是多對一的,只要將None作為值域的一個元素就可以了!我們來看看Haskell中的Maybe型別

data Maybe a = Nothing | Just a

Maybe中有兩個元素,分別是代表”沒有”的Nothing以及”存在”的Just,Just可以想像一個泛型的容器,裡面可以存放一個任意物件。其實如果有對null做好處理的話我覺的跟nullable的物件沒有差異,Maybe更接近概念上的不同,把東西是否存在往上提昇一個層次,null的語意上容易讓人忽略而不處裡,而Nothing作為值域的一個映射結果,跟其他元素應該是需要被同等看待的,此外函數式的語言中有各種的語法糖讓使用者方便的處裡Nothing。

回到C#中可以怎麼處理null呢?

1. Null Object Pattern 空物件模式

空物件模式有很多種應用,其中一個是解決回傳值為null的問題,以前面會員的例子來說或許可以改成:

var person = GetMemberById(id);
Console.WriteLine(person.Name);

Member GetMemberById(long id)
{
		return _dbContext.Member.FisrtOrDefault(x => x.Id == id) ?? new NoMember();
}

public class NoMember() : Member
{

}

因為GetMemberById方法保證了一定有一個會員會被回傳,所以後面印出會員名稱的方法就不會出錯,接下來就是因應商業邏輯,要將這個空物件設計的多麼仔細,盡可能的定義出他的行為,只是這樣就會面臨兩個問題

  1. 空物件的行為太複雜,導致需要花很多精神設計(這時候可能要回過頭檢討類別是不是應該重構)
  2. 不太可能為了所有的方法都寫一個空物件

2. 啟用Null-state analysis

C#後來加入了nullable的特性,並且在程式碼撰寫的時候可以針對null做判斷決定後續的程式碼流程,另外也有。

int? number = null;

if(number.HasValue) Console.WriteLine(number.Value);
else Console.WriteLine("數字不存在");
// Result:數字不存在

//也可以這樣寫
if(number is {} value) Console.WriteLine(value);
else Console.WriteLine("數字不存在");

另外在.Net6中可以在專案設定將Null-state analysis開啟,這樣會在編譯的時候對null reference做檢查,並且有不同的嚴格程度,強烈建議開啟。另外文件上也有提供局部修改null嚴格程度的方法

#nullable enable    
		// 這裡可以寫nullable的型別
#nullable disable   
		// 這邊不能寫nullable的型別
//--------------------------------------
Console.WriteLine(person!.name); // 這邊告訴編譯器person不會是null

3. 自定義Option (Maybe)

在FP的設計風格中,對於Maybe的概念其實有很多不同的名稱,C#有一個針對函數式設計的套件LanguageExt使用Option : Some() | None , Option表示這個值是可選的,如果有值的話包在Some裡面。要實現Option其使可以有各種作法

public abstract class Option
{
    public static Option Some<T>(T data)
    {
        return new Some<T>(data);
    }

    public static Option None()
    {
        return new None();
    }
}

public class Some<T> : Option
{
    public Some(T data)
    {
        this.Data = data;
    }

    public T Data { get; init; }
}

public class None : Option
{
}

public static class OptionExtension
{
    public static Option Map<TSource, TResult>(this Option source, Func<TSource, TResult> operate)
		{
		    return source switch
		    {
		        None => source,
		        Some<TSource> { Data: var sourceData } =>
															Option.Some(operate(sourceData)),
		        _ => throw new NotSupportedException()
		    };
		}
}

這邊是我想像中Option的一種實做,其實少考慮非常多東西。C#過去對FP支援沒那麼多的時候,LanguageExt套件擴充了很多功能,然而隨著C#的發展,本身的語法就已經十分強大,在這邊其實只是想要展示一下OptionExtension中的Map方法,是函數是語言中的重要觀念,另外可以看到switch expression中對於Some的解構,就是C#7/8中加入的功能,也是FP中經常使用的pattern match。

小結

其實觀察Map方法很明顯的可以發現,如果將空值的概念提昇到Option的層級,在撰寫程式碼的時候會有不一樣的想法,我自己覺得Option可以幫助工程師在設計函數的時候就完整的考慮空值的情況,而null的處理比較偏向被動,好處是可以減少很多的程式碼,畢竟實務層面並不是所有的null都有可能導致錯誤,不過另一方面也是很多系統錯誤的原因,但話又說回來,C#的Null Conditional Operators真的很方便,轉個形式其實就很FP了。


上一篇
Day2. 函數的定義域與值域
下一篇
Day4. Pattern Match
系列文
Functional Programming with C#30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言